package Dungeon;

import java.util.Iterator;

import be.kuleuven.cs.som.annotate.*;
import Coordinates.Axis;
import Coordinates.Coordinates;
import Predicate.Predicate;
import Square.*;

/**
 * 
 * @author Koen Certyn & Michiel Meersmans
 */
public abstract class Dungeon<T extends Square> {

	/**
	 * Creates a new dungeon
	 *  
	 * @param 	xDimension
	 * 			The x-dimension
	 * @param 	yDimension
	 * 			The y-dimension
	 * @param 	zDimension
	 * 			The z-dimension
	 * @throws	IllegalArgumentException
	 * 			Throws an exception if one of the given dimensions is not a valid value
	 * 			| ! isValidDungeonDimension(xDimension) || ! isValidDungeonDimension(yDimension) || ! isValidDungeonDimension(zDimension)
	 * @post	The dungeon's x-dimension is set to the given value
	 * 			| new.getDungeonDimension(xDimension, Axis.X_AXIS) == xDimension
	 * @post	The dungeon's y-dimension is set to the given value
	 * 			| new.getDungeonDimension(yDimension, Axis.Y_AXIS) == yDimension
	 * @post	The dungeon's z-dimension is set to the given value
	 * 			| new.getDungeonDimension(zDimension, Axis.Z_AXIS) == zDimension
	 * @post	The dungeon's max dimension value is set too the given value
	 * 			| new.getMaxDimensionValue == maxDimension
	 */
	public Dungeon(long maxDimension, long xDimension, long yDimension, long zDimension) throws IllegalArgumentException
	{
		setMaxDimensionValue(maxDimension);
		setDungeonDimension(xDimension, Axis.X_AXIS);
		setDungeonDimension(yDimension, Axis.Y_AXIS);
		setDungeonDimension(zDimension, Axis.Z_AXIS);
	}
	
	/**
	 * Creates a new dungeon
	 *  
	 * @param 	xDimension
	 * 			The x-dimension
	 * @param 	yDimension
	 * 			The y-dimension
	 * @param 	zDimension
	 * 			The z-dimension
	 * @throws	IllegalArgumentException
	 * 			Throws an exception if one of the given dimensions is not a valid value
	 * 			| ! isValidDungeonDimension(xDimension) || ! isValidDungeonDimension(yDimension) || ! isValidDungeonDimension(zDimension)
	 * @effect	A new dungeon is created with the given dimension and a max dimension value of 20
	 * 			| this(20, xDimension, yDimension, zDimension)
	 */
	public Dungeon(long xDimension, long yDimension, long zDimension) throws IllegalArgumentException {
		this(20, xDimension, yDimension, zDimension);
	}
	
	/**
	 * Sets the maximum dimension value
	 *
	 * @param 		maxDimension
	 * 				Parameter which resembles the new maxDimension
	 * 
	 * @post		The new maxValue is set
	 * 				| new.getMaxDimensionValue() == maxDimension
	 * 	
	 * @throws 		IllegalArgumentException
	 * 				Exception that is thrown when maxDimension is not a valid dimension
	 * 				| ! isValidMaxDimensionValue(maxDimension)
	 */
	@Raw
	public void setMaxDimensionValue(long maxDimension) throws IllegalArgumentException {
		if (isValidDimension(maxDimension))
			this.maxDimensionValue = maxDimension;
		else
			throw new IllegalArgumentException("The given maxdimension is not a valid value");
	}
	
	/**
	 * Variable that holds the maximum value of a dimension
	 * Default initialized on 20
	 */
	private long maxDimensionValue = 20;
	
	/**
	 * Returns the dungeon's max dimension value
	 * 
	 */
	@Basic
	public long getMaxDimensionValue(){
		return this.maxDimensionValue;
	}
	
	/**
	 * Method that checks if the new given maxValue is a valid value.
	 * 
	 * @param 		newMaxValue
	 * 				Parameter that is given of the long type, which resembles the new maxValue
	 * 
	 * @return		Returns true if the new maximum dimension value is bigger then the original value, returns false if
	 * 				the newMaxValue equals or is lower then the current maxValue
	 * 				| result == newMaxValue > getMaxDimensionValue()
	 */
	public boolean isValidMaxDimensionValue(long newMaxValue){
		return (newMaxValue > getMaxDimensionValue());
	}
	
	/**
	 * Parameters that contain the value's (long type) of the maximum size of the dungeon in its 3 different directions
	 */
	private long xDungeonDimension,yDungeonDimension,zDungeonDimension;

	/**
	 * Method that sets the maxDimensions to the given maxDimension
	 * 
	 * @param 		maxDimension
	 * 				Parameter that contains the value of the maxDimension
	 * 
	 * @param 		axis
	 * 				Enum that resembles the axis on which the maxDimension has effect on
	 * 
	 * @post		Sets the new maxDimension value
	 *				| new.getDungeonDimensions(axis) == maxDimension;
	 *
	 * @throws 		IllegalArgumentException
	 * 				Throws a exception if the given maxDimension is not a valid value
	 * 				| ! isValidDungeonDimension(maxDimension)
	 */
	@Raw
	public void setDungeonDimension(long maxDimension, Axis axis)throws IllegalArgumentException{
		if ( ! isValidDungeonDimension(maxDimension) )
			throw new IllegalArgumentException();
		if (axis == Axis.X_AXIS)
			this.xDungeonDimension = maxDimension;
		else if (axis == Axis.Y_AXIS)
			this.yDungeonDimension = maxDimension;
		else
			this.zDungeonDimension = maxDimension;
	}
	
	/**
	 * Checks if the given dimension is valid for this dungeon
	 * 
	 * @param 		dimension
	 * 				The dimension to check
	 * 
	 * @return		Returns true if the given dimension is a valid one for this dungeon.
	 * 				| result == ((dimension > 0) && (dimension <= getMaxDimensionValue()))
	 */
	public boolean isValidDungeonDimension(long dimension){
		return ((dimension > 0) && (dimension <= getMaxDimensionValue()));
	}
	
	/**
	 * Getter method that returns the asked value of dimension
	 * 	
	 * @param 		axis
	 * 				Enum that resembles the axis
	 */
	@Basic
	public long getDungeonDimensions(Axis axis){
		if (axis == Axis.X_AXIS)
			return this.xDungeonDimension;
		else if (axis == Axis.Y_AXIS)
			return this.yDungeonDimension;
		return this.zDungeonDimension;
	}
	
	/**
	 * Checks if the value is a valid dimension
	 *  
	 * @param		dimension
	 * 				The dimension to check
	 * 
	 * @return		Returns true if the dimensions are positive
	 * 				| result == dimension > 0
	 */
	public boolean isValidDimension(long dimension){
		return dimension > 0 ;
	}
	
	/**
	 * Returns the square from the given coordinates in this dungeon.
	 * @param 	x
	 * 			X-Coordinate to get the square from.
	 * @param 	y
	 * 			Y-Coordinate to get the square from.
	 * @param 	z
	 * 			Z-Coordinate to get the square from.
	 * @note	Returns null if there's no square at given coordinates
	 * 
	 */
	@Basic
	abstract public T getSquare(long x, long y, long z);
	
	/**
	 * Returns the dungeon that holds this dungeon as sub-dungeon
	 * 
	 */
	@Basic
	public CompositeDungeon<? super T> getParentDungeon()
	{
		return parentDungeon;
	}
	
	/**
	 * Returns this dungeon's root dungeon
	 * 
	 * @return 	This dungeon's root dungeon, returns null if this dungeon doesn't have root dungeon
	 * 			| if(getParentDungeon() != null)
	 *			|   dungeon = getParentDungeon()
	 *			| 	while(dungeon != null)
	 *			|	{
	 *			|		if(dungeon.getRootDungeon() == null)
	 *			|			result == dungeon
	 *			|		dungeon = dungeon.getRootDungeon()
	 *			|	}
	 *			|	result == null
	 */
	public CompositeDungeon<? super T> getRootDungeon()
	{
		if(getParentDungeon() != null)
		{
			CompositeDungeon<? super T> dungeon = getParentDungeon();
			while(dungeon != null)
			{
				if(dungeon.getParentDungeon() == null)
					return dungeon;
				dungeon = dungeon.getParentDungeon();
			}
		}
		return null;
	}
	
	
	/**
	 * Method that returns the distance to the root dungeon
	 * 
	 * @return	A new coordinate object containing the distance from this dungeon's origin to the root dungeon's origin
	 */
	public Coordinates getDistanceToRootOrigin()
	{
		long x = 0,y = 0,z = 0;
		
		if(getParentDungeon() != null)
		{
			CompositeDungeon<? super T> dungeon = getParentDungeon();
			x += dungeon.getOriginFromDungeon(this).getX();
			y += dungeon.getOriginFromDungeon(this).getY();
			z += dungeon.getOriginFromDungeon(this).getZ();
			while(dungeon != null)
			{
				if(dungeon.getParentDungeon() == null)
					return new Coordinates(x, y, z);
				else
				{
					CompositeDungeon<? super T> parent = dungeon.getParentDungeon();
					Coordinates origin = parent.getOriginFromDungeon((Dungeon) dungeon);
					x += origin.getX();
					y += origin.getY();
					z += origin.getZ();
					dungeon = parent;
				}
			}
		}
		return new Coordinates(x, y, z);
	}
	
	/**
	 * Sets a parent dungeon for this dungeon
	 * 
	 * @param 	compositeDungeon
	 * 			The parent dungeon
	 * @pre		The dungeon must contain this dungeon
	 * 			| dungeon.hasDungeon(this)
	 * @pre		The dungeon must be effective
	 * 			| dungeon != null
	 * @post	This dungeon's parent dungeon is set to the given value
	 * 			| new.getParentDungeon() == dungeon
	 */
	public void setParentDungeon(CompositeDungeon<? super T> compositeDungeon)
	{
		assert compositeDungeon.hasDungeon( this);
		assert compositeDungeon != null;
		this.parentDungeon = compositeDungeon;
	}
	
	
	/**
	 * A variable containing the parent dungeon
	 */
	private CompositeDungeon<? super T> parentDungeon = null;
	
	/**
	 * Returns a iterator
	 */

	abstract public Iterator<T> getIterator();
	/**
	 * Returns a filtered iterator with given predicate
	 */
	abstract public Iterator<T> getIterator(Predicate<T> predicate);
	
	
}
